Basic Game Tutorial 5: Animation
Hello yet again! Congratulations on your diligent hard work, determination and commitment to improving your skills. You're great.
In this part of the Game tutorial, we'll really be bringing this project to life. It's all well and good having a moving character jumping and walking on platforms, but without animations things look rather bland.
We'll be covering the basics of character animations in this tutorial and as always, the concepts used here can be applied to absolutely any project.
To make the character animations work smoothly and simply, we'll be creating something called a state machine.
What is a State Machine?
It's a cool way of saying that we're keeping track of the player's current state. At any time, the player might be idle (standing still), walking, jumping or being hit by an enemy. If we keep track of what state the player is currently in, we can use this information to make animating the player very simple.
First, as always, let's create some variables! We'll need 6 variables here. Add the following lines to the top of your program, just beneath the moveSpeed
variable:
8. moveSpeed = 5
9.
10. idle = 0
11. walk = 1
12. jump = 2
13. hit = 3
14.
15. state = idle
Now we have a variable for each state the player might be in. As you can see, each one holds a different number. We will use this number as an index into an array of animations.
We have also created a variable called state
which we will use to store the player's state.
Now let's create the array of animation information, just beneath these variables:
17. anim = [
18. [ .start = 96, .length = 1 ],
19. [ .start = 97, .length = 11 ],
20. [ .start = 95, .length = 1 ],
21. [ .start = 94, .length = 1 ]
22. ]
There we have it! In this array of structures, we store the start tile of each state's animation. The idle animation for the player is a single frame with a tile number of 96, so the .start
property is 96, and the .length
property is 1. Here's an image to detail why we use these numbers in particular:
As you can see, the walk animation begins at tile 96 and lasts for 11 frames. The jump animation begins at tile 95 and lasts for only one frame.
We have created this array in the same order as the state variables. Because of this, we can now access any of these state animations with a statement like:
print( anim[walk].start )
Clever right!
Now let's put these to use. We have one last variable to create first, which will store the current animation frame. We'll call this animationFrame
and it will be defined just after the anim
array:
24. animationFrame = 0
We're all set!
Time to use these variables in the drawSheet()
function used to draw the player. Take a look at the end of the main game loop:
102. drawSheet( chrSheet, 96, playerX, playerY, scale )
Currently we are using a single fixed value for the tile. Rather than the number 96, we need to use a variable instead in order to change this during the game. Create a line just above the drawSheet()
function and define the following variable:
102. animationStart = anim[state].start
103.
104. drawSheet( chrSheet, 96, playerX, playerY, scale )
Our variable is called animationStart
and it will store the starting frame of animation for our states. This variable isn't totally necessary as we can simply use anim[state].start
, but it makes our code easier to read.
Remember the animationFrame
variable we created earlier? It's time to put this to use:
104. drawSheet( chrSheet, animationStart + animationFrame, playerX, playerY, scale )
All we have done for this change is swapped out the 96
in our drawSheet()
function for animationStart + animationFrame
.
This is a very helpful way of changing the tile shown for the player. All we need to do now is increase the animationFrame
variable and our tile will animate!
104. drawSheet( chrSheet, animationStart + animationFrame, playerX, playerY, scale )
105.
106. animationFrame += 0.2
Run the program to see something very strange!
Our character animates, but then turns into other characters and completely different tiles until we get an error. Do you understand why this is happening?
We are increasing the tile past the point we want and displaying tiles that aren't in the correct range.
With a couple of if statements we can solve this! Create a few lines of space above the drawSheet()
line, and add the following if statement:
104. if animationFrame >= anim[state].length then
105. animationFrame = 0
109. endif
107.
108. drawsheet( chrSheet, animation + animationFrame, playerX, playerY, scale )
When we run the program our tile will no longer change. This actually means it's working correctly!
The animation for the idle state is just a single frame. In the animation array, the idle state animation has a .start
of 96
and a .length
of 1
.
This means the animationFrame
variable never gets above 1, therefore we only see a single frame.
Time to put the state machine to use! Our animation array stores the start and end tiles of each set of animations for each state of the character. All we need to do now is change the player's state!
We must set the state at various points in our program. Remember, we set the state at the start of the program as idle by default. Let's check the first place to change it:
73. if c.a and jumpTimer < 12 then
74. jumpTimer += 1
75. velocity -= 8 / jumpTimer
76. state = jump
77. endif
Right here seems like a good place! When the character jumps into the air, we need the state to change in order to see the jump frame.
Run the program and jump to see if it works!
If it's working properly, our character should change to the jump frame but they will not change back.
To make the character go back the idle frame, we just need to switch the state back to idle when they land:
87. if !collision( playerX + tSize / 2, playerY + tSize + velocity ) then
88. playerY += velocity
89. else
90. playerY = int( ( playerY + velocity ) / tSize ) * tSize
91. velocity = 0
92. jumpTimer = 0
93. state = idle
94. endif
Above is the if statement which causes the character to fall through empty space and land on platforms. Below the else
is what will happen when our character lands on a platform tile. Here we just need to add state = idle
and we're done!
Run the program and see our character's glorious jump! Truly a more majestic jump has never been seen.
All that's left is to make our character walk. We already have all the data we need - we just need to change the state
variable to walk
. We need to add something to our left and right movement if statements.
96. if c.right and !collision( playerX + tSize / 2 + moveSpeed, playerY + tSize -1 ) then
97. playerX += moveSpeed
98. if state != jump then
99. state = walk
100. endif
100. endif
We have added lines 98 to 100 above in the first of the movement if statements. We want to set the state to walk
when we press the right or left directional buttons, but only if we are not already jumping. Therefore we must write:
if state != jump then
state = walk
endif
Now let's add the exact same thing to left movement if statement:
103. if c.left and !collision( playerX + tSize / 2 - moveSpeed, playerY + tSize - 1 ) then
104. playerX -= moveSpeed
105. if state != jump then
106. state = walk
107. endif
108. endif
That's it! Our player animation is complete! Run the program and move the character around to see the results.
There is one last little task we must accomplish before moving to the next stage however. At the minute, we can move the character, jump and land on platforms, but when we travel to the right side of the screen, the camera doesn't move to reveal the rest of the level! This just won't do.
We already have the variables we'll need, we just need to put them to use. Add the lines below to your program:
61. if playerX - screenX < screenW * 0.4 then
62. screenX -= moveSpeed
63. endif
64. if playerX - screenX > screenW * 0.6 then
65. screenX += moveSpeed
66. endif
67. if screenX < 0 then
68. screenX = 0
69. endif
Run the program once you've added the lines above and travel to the right side of the screen. We should see the background move, but not the level just yet!
This is because we need to modify our level drawing position to be relative to the screenX
variable.
Go to the for loop which draws the level and add the change below:
73. for row = 0 to len( level ) loop
74. for col = to len( level[0] ) loop
75. if level[row][col] >= 0 then
76. x = col * tSize
77. y = ( row + levelOffset ) * tSize
78. drawSheet( tilesheet, tiles[level[row][col]], x - screenX, y, scale )
79. endif
80. repeat
81. repeat
Can you spot the change? It's not very obvious. On line 78, we must add a - screenX
to the x position argument of the drawSheet()
function. This will cause the level to be drawn relative to the movement of our screen.
We must also do this same thing for the player or we'll encounter some strange problems. Find the drawSheet()
line for the player and add the same change:
126. drawSheet( chrSheet, animationStart + animationFrame, playerX - screenX, playerY, scale )
Run the program and travel to the right to see the level and background and level move with the player to reveal the rest of the level.
Wouldn't it be nice if the background image moved at a different speed than the level? This way, it would really look as though the background was in the distance! We can do this very easily. Find the drawImage()
line which draws the background:
71. drawImage( background, -screenX / 2, - screenY, screenH / imageSize( background ).y )
We simply add a / 2
to the x position! Now our background will move at half the speed of the foreground.
Run the program to see a wonderful moving level. All done!
The Program So Far
As always, we have a complete and up-to-date version of the whole program so far just below. If your program is not working and you cannot figure it out, feel free to copy and paste this code into a new project file:
1. background = loadImage( "Kenney/backgrounds", false )
2. tilesheet = loadImage( "Kenney/superPlatformPack", false )
3. chrSheet = loadImage( "Kenney/characters", false )
4.
5. playerX = 0
6. playerY = 0
7.
8. moveSpeed = 5
9.
10. idle = 0
11. walk = 1
12. jump = 2
13. hit = 3
14.
15. state = idle
16.
17. anim = [
18. [ .start = 96, .length = 1 ],
19. [ .start = 97, .length = 11 ],
20. [ .start = 95, .length = 1 ],
21. [ .start = 94, .length = 1 ]
22. ]
23.
24. animationFrame = 0
25.
26. gravity = 1
27. velocity = 0
28.
29. jumpTimer = 0
30. oldA = 0
31.
32. screenX = 0
33. screenY = 0
34.
35. tiles = [ 121, 138, 128, 129, 130 ]
36.
37. level = [
38. [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ],
39. [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ],
40. [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ],
41. [ 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
42. [ 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
43. [ 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
44. ]
45.
46. levelHeight = 12
47. levelOffset = levelHeight - len( level )
48. tSize = 0
49.
50. loop
51. clear()
52.
53. c = controls( 0 )
54.
55. screenW = gwidth()
56. screenH = gheight()
57. scale = screenH / ( tileSize( tilesheet, 121 ).y * levelHeight )
58. tSize = scale * tileSize( tilesheet, 121 ).y
59. pSize = tileSize( chrSheet, 96 ) * scale
60.
61. if playerX - screenX < screenW * 0.4 then
62. screenX -= moveSpeed
63. endif
64. if playerX - screenX > screenW * 0.6 then
65. screenX += moveSpeed
66. endif
67. if screenX < 0 then
68. screenX = 0
69. endif
70.
71. drawImage( background, -screenX / 2, -screenY, screenH / imageSize( background ).y )
72.
73. for row = 0 to len( level ) loop
74. for col = 0 to len( level[0] ) loop
75. if level[row][col] >= 0 then
76. x = col * tSize
77. y = ( row + levelOffset ) * tSize
78. drawSheet( tileSheet, tiles[level[row][col]], x - screenX, y, scale )
79. endif
80. repeat
81. repeat
82.
83. if c.a and jumpTimer < 12 then
84. jumpTimer += 1
85. velocity -= 8 / jumpTimer
86. state = jump
87. endif
88.
89. if oldA and !c.a then
90. jumpTimer = 12
91. endif
92.
93. oldA = c.a
94.
95. velocity += gravity
96.
97. if !collision( playerX + pSize.x / 2, playerY + pSize.y + velocity ) then
98. playerY += velocity
99. else
100. playerY = int( ( playerY + velocity + pSize.y ) / tSize ) * tSize - pSize.y
101. velocity = 0
102. jumpTimer = 0
103. state = idle
104. endif
105.
106. if c.right and !collision( playerX + pSize.x / 2 + moveSpeed, playerY + pSize.y - 1 ) then
107. playerX += moveSpeed
108. if state != jump then
109. state = walk
110. endif
111. endif
112.
113. if c.left and !collision( playerX + pSize.x / 2 - moveSpeed, playerY + pSize.y - 1 ) then
114. playerX -= moveSpeed
115. if state != jump then
116. state = walk
117. endif
118. endif
119.
120. animationStart = anim[state].start
121.
122. if animationFrame >= anim[state].length then
123. animationFrame = 0
124. endif
125.
126. drawSheet( chrSheet, animationStart + animationFrame, playerX - screenX, playerY, scale )
127.
128. animationFrame += 0.2
129.
130. update()
131. repeat
132.
133. function collision( x, y )
134. tileX = int( x / tSize )
135. tileY = int( y / tSize ) - levelOffset
136.
137. result = true
138.
139. if tileY < 0 or tileY >= len( level ) or tileX < 0 or tileX >= len( level[0] ) then
140. result = false
141. else
142. if level[tileY][tileX] < 0 then
143. result = false
144. endif
145. endif
146. return result
Functions and Keywords used in this tutorial
clear(), controls(), drawImage(), drawSheet(), else, endIf, for, function, gHeight(), gWidth(), if, int(), len(), loadImage(), loop, repeat, return, tileSize(), then, to, update()